模块化规范
0.IIFE
1 | const myModule = (function(...deps){ |
该方法只是把变量和方法都封装在了本身作用域内的一种模式,并没有构成处理依赖能力的模块。在一定程度上解决了作用域污染的问题。
1.CommonJS规范
Node.js平台的默认格式,可以在Node平台上运行,为了统一JavaScript在浏览器之外的实现,CommonJS诞生了。通常的写法是这样的:
1 | //fileA.js |
CommonJS规范是为了解决JS的作用域问题而定义的模块模式,可以使每个模块在自身的命名空间中执行。模块必须通过module.exports
导出对外的变量或接口,通过require()
来导入其他模块的输出到当前模块作用域中。
CommonJS是动态加载即运行时加载方案,在并不需要完全加载所有方法的前提下,仍然会加载所有的模块。如下例所示:
1 | //CommonJS模块 |
2.AMD规范
1 | //AMDTest.html |
而AMD方案也是一种动态异步加载方案,它们的API语义只有在运行时才会被考虑进来,因此可以在运行时修改一个模块的API。不管是CMD还是AMD规范,都是讲模块定义封装在一个API中,简要的说明该方案的核心概念:
1 | var MyModule = (function Manager(){ |
为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的API。储存在根据名字来管理的模块列表中。在yourName.js
中可以以jQuery
实例作为依赖参数传入,并且相应的使用它。
3. CMD规范
并没有使用过UMD规范的模块化方案,但是定义方法与AMD相差不大,对于依赖的模块,AMD依赖前置,CMD依赖就近。
4. ES6 Module
ES6模块的设计思想是尽量静态化,在编译时就确定好模块之间的依赖关系,避免在运行时才抛出错误。除此之外,由于是静态加载,在CommonJS
的例子中,如果改写为ES6模块:
1 | import { state,exists,readFile } from "fs"; |
以上的实质是从fs
模块中加载3个方法,其他方法并不会加载。所以加载的效率也更高。也不需要看上去冗余的define
关键字来定义模块模式了。
ES6 Module命令
export
一个模块是一个独立的文件,该模块内部的所有变量,外部无法获取。如果希望其他的模块能够读取该模块内部的某个变量,就必须使用export
关键字输出变量。
1 | // profile.js |
除了输出变量,还可以输出函数或类(class)。
1 | export function add(a,b){ |
除了使用以上的方式暴露变量/函数外,还可以使用as
关键字。
1 | function v1() { ... } |
export
命令可以处于模块的任何位置。但出现在块级作用域内就会报错,因为处于条件代码块中,无法做静态优化,违背了ES6 Module的设计初衷。
import命令
使用了export
定义了对外接口以后,其他JS文件可以使用import
命令加载模块。
1 | // main.js |
注意 : import关键字后{}中的变量名必须与export输出的变量名相同。
由于import
是静态执行的,所以不能使用表达式和变量,他们是只能在运行时得到结果的语法结构。
最后,当使用ES6 Module时要注意script
中的type
属性:
1 | //注意:type="module" |
如果直接运行文件,会因为file://
文件域的跨域问题出现报错,此时需要开一个server服务器,建议使用http-server
:
1 | npm install http-server |
export default命令
当使用import
命令时,需要知道加载的变量名或函数名。通过default
命令,可以在不需要知道函数或变量名名称的前提下,默认输出变量。
1 | //export-defult.js |
以上的代码中模块文件export-default.js
默认输出一个函数,其他模块加载时,import
命令可以为该匿名函数指定任意名字。
比如,加载一些常用的模块:
1 | import $ from 'jquery';//加载jQuery库 |
注意,此时引入模块时,不需要使用大括号。
除此之外,export default
也可以用在命名函数前。
1 | export defualt function foo(){ |
export default
命令用于指定模块的默认输出,一个模块只能有一个默认输出。因此export default
命令只能使用一次,所以import
命令后才不用加大括号,因为只会对应一个方法。
通配符
除了使用指定变量名或export default
定义的导出,还可以使用*
通配符加载模块的全部。
1 | //math.js |
as关键字
允许在模块输入或者输出时使用as关键字修改名字。
1 | //输入: |
特性
import导入模块只读
在CommonJS中,导入的模块是导出值的复制值,并且require动作是同步的。所以导入与导出之间的联系是不存在连接的。
在ES6中,到日对导出值是只读的。因此他们的关系是赋址。并且”只读”说明在导入的模块中不能直接修改被导入的值,如果要修改被导入的值,可以通过调用被导入模块的函数来达到目的。
例如:
1 | //lib.js |
支持循环依赖
模块A、B之间互相导入,并在其中调用两者之间的函数,形成循环调用。
以ComonJS
为例:
1 | //a.js |
在b模块成功导入a之前,b模块不能使用a模块的方法。而在b模块导入a模块时,a需要先加载完成自身的模块依赖,这时a执行var b = require(b)
;
而ES6自动支持循环依赖。import
对导入的模块是只读的。所以在执行过程中可以间接调用导入的值。
//a.js
import {bar} from './b';
export function foo(){
bar();
}
//b.js
import {foo} from 'a';
export function bar(){
if(Math.random()){
foo();
}
}
结论
与其他规范的模块化规范相比,ES6 Module更强调静态化加载。这样做带来的优点有:
- 1. 静态加载效率更高(能够实现按需加载)
- 2. 未来引入宏、类型检查等特性(静态化)
- 3. 前后端统一模块化标准
- 4. 未来浏览器API及扩展功能通过模块提供
目前,通过http-server
,ES6 Module可以在原生浏览器中直接运行。结合babel及webpack,ES6 Module能够提供更兼容、友好的模块化方案,而babel
及webpack
在这其中扮演了什么样的角色,在下一篇文章中会做出阐述。